<?php
/*
 * Copyright (c) 2013 Eugene Kuzmenko
 *
 * Permission  is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in  the Software without restriction, including without limitation the rights
 * to  use,  copy,  modify,  merge, publish, distribute, sublicense, and/or sell
 * copies  of  the  Software,  and  to  permit  persons  to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The  above  copyright  notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE  IS  PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED,  INCLUDING  BUT  NOT  LIMITED  TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS  FOR  A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS  OR  COPYRIGHT  HOLDERS  BE  LIABLE  FOR  ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * For more information, see <http://www.aljey.com>.
 */

namespace Al\Core\Lib;

use Al\Core\Exception\FileNotFoundException;

/**
 * Facilitates all the tools necessary to work with yaml files, gives you the
 * ability to #import external files, have system-wide parameters, employs
 * automatic caching to improve performance and opts for using a yaml php pecl
 * extension as a backend if available.
 *
 * @author Eugene Kuzmenko <mail@aljey.com>
 */
class YAML {

  /**
   * @var array The micro-cache metadata array.
   */
  private $meta = array();

  /**
   * @var \Spyc An instance of the spyc parser.
   */
  private $spyc;

  /**
   * Converts an array into a yaml string.
   * Uses a more efficient yaml php pecl extension if available, to install it
   * execute the following commands in terminal:
   * <code>
   * sudo apt-get install php5-dev
   * sudo apt-get install libyaml-dev
   * sudo pecl install yaml
   * // add extension=yaml.so to php.ini and restart the server
   * </code>
   *
   * @param array $array PHP array.
   * @return string Yaml data string.
   */
  public function dump($array) {
    return trim(extension_loaded('yaml') ? yaml_emit($array) : $this->getSpyc()
      ->dump($array, 2, 0), "-\n.");
  }

  /**
   * Parses a yaml file and returns the resulting array.
   * Uses a more efficient yaml php pecl extension if available, to install it
   * execute the following commands in terminal:
   * <code>
   * sudo apt-get install php5-dev
   * sudo apt-get install libyaml-dev
   * sudo pecl install yaml
   * // add extension=yaml.so to php.ini and restart the server
   * </code>
   *
   * @param string $filename The path to a yaml file.
   * @param callable $callback [optional] A function to execute prior to saving
   *                           the results of the parsing into the cache,
   *                           receives one argument - the parsed array.
   * @return array The result of the yaml file parsing.
   * @throws FileNotFoundException
   */
  public function load($filename, $callback = null) {
    if (!is_file($filename)) {
      throw new FileNotFoundException($filename);
    }
    $filename = realpath($filename);
    $hash = md5($filename);
    $cache = Service::get('cache');
    if (!$this->isModified($filename) && isset($cache[$hash])) {
      return $cache[$hash];
    }
    $content = $this->resolveImports($filename);
    $cache['yml'] = $this->getMeta();
    $yml = $this->parse($content);
    if (is_callable($callback)) {
      $callback($yml);
    }
    $cache[$hash] = $yml;
    return $yml;
  }

  /**
   * Parses a yaml string.
   *
   * @param string $content
   * @return array
   */
  public function parse($content) {
    return extension_loaded('yaml') ? yaml_parse($content) : $this->getSpyc()
      ->load($content);
  }

  /**
   * Parses a yaml file to resolve all the #import calls.
   *
   * Example:
   * <code>
   * routes:
   *   #import routes.yml
   * </code>
   * You can place the #import modifier anywhere within your yaml files,
   * effectively importing any specified external yaml file.
   *
   * @param string $filename The path to a yaml file.
   * @return string Yaml data string.
   * @throws FileNotFoundException
   */
  public function resolveImports($filename) {
    $filename = realpath($filename);
    $hash = md5($filename);
    $dirName = dirname($filename);
    $meta = &$this->getMeta();
    $meta[$hash] = array(
      'time' => filemtime($filename),
      'map' => array(),
      'params' => array(),
    );
    $me = $this;
    return preg_replace_callback('/( *)#import +([^\s]+)/',
      function ($matches) use ($filename, &$meta, $dirName, $hash, &$me) {
        if (!is_file($filename = "$dirName/$matches[2]")) {
          throw new FileNotFoundException($filename);
        }
        if (!in_array($filename, $meta[$hash]['map'], true)) {
          $meta[$hash]['map'][] = $filename;
        }
        return $matches[1] . preg_replace('/\n/', "\n$matches[1]", $me
            ->resolveImports($filename));
      }, preg_replace_callback('/%([^%]+)%/', function ($matches) use (&$meta,
          $hash) {
        return $meta[$hash]['params'][$matches[1]] = Service::get(
          "config.params.$matches[1]");
      }, preg_replace('/__DIR__/', $dirName, file_get_contents($filename))));
  }

  /**
   * Returns the automatic micro-cache metadata.
   *
   * @return array The micro-cache metadata array.
   */
  private function &getMeta() {
    if (!$this->meta) $this->meta = Service::get('cache.yml', array());
    return $this->meta;
  }

  /**
   * Returns an initialized Spyc object.
   *
   * @return \Spyc The spyc instance.
   */
  private function getSpyc() {
    if (!$this->spyc) {
      require_once __DIR__ . '/../Vendor/Spyc/Spyc.php';
      $this->spyc = new \Spyc;
    }
    return $this->spyc;
  }

  /**
   * Checks whether the file or one of the files that it #import's has been
   * modified since the last check.
   *
   * @param string $filename The path to a yaml file.
   * @return boolean Has the file been modified?
   */
  private function isModified($filename) {
    $filename = realpath($filename);
    $hash = md5($filename);
    $meta = &$this->getMeta();
    if (!isset($meta[$hash])) {
      $meta[$hash] = array('time' => 0, 'map' => array(), 'params' => array());
      return true;
    }
    if (!isset($meta[$hash]['params'])) {
      $meta[$hash]['params'] = array();
    }
    foreach ($meta[$hash]['params'] as $key => $value) {
      if ($value != Service::get("config.params.$key")) {
        return true;
      }
    }
    if (!isset($meta[$hash]['time'])) {
      $meta[$hash]['time'] = 0;
    }
    $time = filemtime($filename);
    if ($time > $meta[$hash]['time']) {
      $meta[$hash]['time'] = $time;
      return true;
    }
    if (!isset($meta[$hash]['map'])) {
      $meta[$hash]['map'] = array();
    }
    foreach ($meta[$hash]['map'] as $value) {
      if ($this->isModified($value)) {
        return true;
      }
    }
    return false;
  }

}